/*
 * Copyright (C) 2009 Leandro de Oliveira Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.japa.repository.collection.jpa;

import java.util.*;

import javax.persistence.*;

import org.slf4j.*;
import org.springframework.dao.*;
import org.springframework.orm.jpa.*;

import com.google.code.japa.repository.collection.*;
import com.google.code.japa.util.*;

/**
 * Base class for persistent collection strategy implementations based on JPA.
 * 
 * @param <T> Type of the entity whose strategy manipulates the extent.
 * 
 * @author Leandro Aparecido
 * @since 1.0
 * @see com.google.code.japa.repository.collection.PersistentCollectionStrategy
 */
public abstract class AbstractJpaPersistentCollectionStrategy<T> implements PersistentCollectionStrategy<T> {
	
	private static final long serialVersionUID = 5327020698013910663L;

	private static final Logger logger = LoggerFactory.getLogger(AbstractJpaPersistentCollectionStrategy.class);

	/**
	 * Type of the entity whose strategy manipulates the extent.
	 */
	private Class<T> entityType;
	
	/**
	 * Helper to execute JPA operations.
	 */
	private transient JpaOperations jpaOperations;

	/**
	 * Entities that have been added to the collection since the last flush or collection creation.
	 */
	private List<T> addedEntities;

	/**
	 * Entities that have been updated on the collection since the last flush or collection creation.
	 */
	private List<T> updatedEntities;

	/**
	 * Entities that have been removed from the collection since the last flush or collection creation.
	 */
	private List<T> removedEntities;

	/**
	 * Creates the strategy with the specified parameters.
	 * 
	 * @param entityType Type of the entity whose strategy manipulates the extent.
	 * @param jpaOperations Helper to execute JPA operations.
	 */
	public AbstractJpaPersistentCollectionStrategy(Class<T> entityType, JpaOperations jpaOperations) {
		this.entityType = entityType;
		this.jpaOperations = jpaOperations;

		this.addedEntities = new ArrayList<T>();
		this.updatedEntities = new ArrayList<T>();
		this.removedEntities = new ArrayList<T>();
	}

	/**
	 * {@inheritDoc}
	 */
	public void add(int index, T entity) {
		if (addedEntities.contains(entity)) {
			logger.warn("Potential duplicate key, entity [{}] is being marked for insert more than once");
		} else if (updatedEntities.contains(entity)) {
			logger.warn("Potential duplicate key, entity [{}] has been marked for explicit update and now is being marked for insert");
		} else if (removedEntities.contains(entity)) {
			throw new InvalidDataAccessApiUsageException("Cannot mark entity [{}] for insert after marking it for delete");
		}
		addedEntities.add(entity);
		
		doAdd(index, entity);
	}

	/**
	 * {@inheritDoc}
	 */
	public T set(int index, T entity) {
		T aux = doSet(index, entity);
		
		if (!updatedEntities.contains(entity)) {
			updatedEntities.add(entity);
		}
		
		return aux;
	}

	/**
	 * {@inheritDoc}
	 */
	public T get(int index) {
		return doGet(index);
	}

	/**
	 * {@inheritDoc}
	 */
	public T remove(int index) {
		T aux = doRemove(index);
		removedEntities.add(aux);
		
		return aux;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean remove(T entity) {
		if (entity == null) {
			return false;
		}
		
		boolean removed = doRemove(entity);
		if (removed) {
			removedEntities.add(entity);
		}
		
		return removed;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean contains(T entity) {
		if (entity == null) {
			return false;
		}
		
		return doContains(entity);
	}

	/**
	 * {@inheritDoc}
	 */
	public int size() {
		return doSize();
	}

	/**
	 * {@inheritDoc}
	 */
	public void flush() {
		boolean shouldNotify = true;
		if (addedEntities.isEmpty() && updatedEntities.isEmpty() && removedEntities.isEmpty()) {
			shouldNotify = false;
		}

		persist(addedEntities);
		addedEntities.clear();
		
		merge(updatedEntities);
		updatedEntities.clear();
		
		remove(removedEntities);
		removedEntities.clear();

		if (shouldNotify) {
			flushed();
		}
	}
	
	/**
	 * Persists the entities to the JPA repository.
	 * 
	 * @param entitiesToPersist Entities to persist.
	 */
	protected void persist(List<T> entitiesToPersist) {
		logger.debug("Persisting [{}] entities of type [{}]", entitiesToPersist.size(), entityType.getName());
		for (T entity : entitiesToPersist) {
			getJpaOperations().persist(entity);
		}
	}

	/**
	 * Merges the entities to the JPA repository.
	 * 
	 * @param entitiesToMerge Entities to merge.
	 */
	protected void merge(List<T> entitiesToMerge) {
		logger.debug("Merging [{}] entities of type [{}]", entitiesToMerge.size(), entityType.getName());
		for (T entity : entitiesToMerge) {
			getJpaOperations().merge(entity);
		}
	}

	/**
	 * Removes the entities from the JPA repository.
	 * 
	 * @param entitiesToRemove Entities to remove.
	 */
	protected void remove(List<T> entitiesToRemove) {
		logger.debug("Removing [{}] entities of type [{}]", entitiesToRemove.size(), entityType.getName());
		for (T entity : entitiesToRemove) {
			getJpaOperations().remove(entity);
		}
	}

	/**
	 * Throw {@link ArrayIndexOutOfBoundsException} if index is not between 0 and {@link #size()}.
	 * 
	 * @param index Index to verify.
	 */
	protected void verifyBounds(int index) {
		if (index < 0 || index > size()) {
			throw new ArrayIndexOutOfBoundsException(index);
		}
	}
	
	/**
	 * Removes the specified entity from the buffer or finds it by doing a sequential
	 * search and removes it.
	 * 
	 * @param buffer Buffer to search for.
	 * @param entity Entity to remove.
	 * @return True if removed.
	 */
	protected boolean doBufferOrSequentialRemove(Map<Integer, T> buffer, T entity) {
		boolean removed = false;
		
		if (buffer.containsValue(entity)) {
			Iterator<Map.Entry<Integer, T>> iterator = buffer.entrySet().iterator();
			while (iterator.hasNext()) {
				Map.Entry<Integer, T> entry = iterator.next();
				if (entry.getValue().equals(entity)) {
					remove(entry.getKey());
					removed = true;
					break;
				}
			}
		} else {
			for (int i = 0; i < size(); i++) {
				T aux = get(i);
				if (entity.equals(aux)) {
					remove(i);
					removed = true;
					break;
				}
			}
		}
		
		return removed;
	}
	
	/**
	 * Searches the specified buffer for the entity or finds it by doing a sequential search.
	 * 
	 * @param buffer Buffer to search for.
	 * @param entity Entity to verify.
	 * @return True if contains.
	 */
	protected boolean doBufferOrSequentialContains(Map<Integer, T> buffer, T entity) {
		boolean contains = buffer.containsValue(entity);
		
		if (!contains) {
			for (int i = 0; i < size(); i++) {
				T aux = get(i);
				if (entity.equals(aux)) {
					contains = true;
					break;
				}
			}
		}
		
		return contains;
	}

	/**
	 * Counts the number of entities of the {@link #entityType} in the JPA repository executing
	 * a query.
	 * 
	 * @return Number of entities.
	 */
	@SuppressWarnings("unchecked")
	protected int count() {
		List<Long> list = getJpaOperations().find("SELECT COUNT(c) FROM " + getEntityType().getName() + " c");
		if (list.isEmpty() || list.size() > 1) {
			throw new EmptyResultDataAccessException("Could not count number of entities of type [" + getEntityType() + "]", 1);
		}
		return list.get(0).intValue();
	}

	/**
	 * Query the JPA repository for the entities between the specified limits ordered by entity identity.
	 * 
	 * @param startPosition Position of the first entity in the result.
	 * @param maxResult Maximum number of entities to be returned.
	 * @return List of entities retrieved.
	 */
	@SuppressWarnings("unchecked")
	protected List<T> select(final int startPosition, final int maxResult) {
		return getJpaOperations().executeFind(new JpaCallback() {
			public Object doInJpa(EntityManager em) throws PersistenceException {
				Query query = em.createQuery(createSelect());
				
				if (startPosition >= 0) {
					query.setFirstResult(startPosition);
				}
				if (maxResult > 0) {
					query.setMaxResults(maxResult);
				}

				return query.getResultList();
			}
		});
	}

	/**
	 * Creates a query to select the entities of type {@link #entityType} ordered by entity identity.
	 * 
	 * @return JPA query.
	 */
	protected String createSelect() {
		StringBuilder sb = new StringBuilder();
		sb.append("SELECT c FROM ");
		sb.append(getEntityType().getName());
		sb.append(" c");
		
		String[] columnNames = JpaUtils.getPrimaryKeyNames(getEntityType());
		if (columnNames.length > 0) {
			sb.append(" ORDER BY ");
			for (int i = 0; i < columnNames.length; i++) {
				sb.append(columnNames[i]);
				if (i + 1 < columnNames.length) {
					sb.append(", ");
				}
			}
		}

		return sb.toString();
	}
	
	/**
	 * Add the entity to the extent without flushing changes.
	 * 
	 * @param index Position of the entity in the extent.
	 * @param entity Entity to be added.
	 */
	protected abstract void doAdd(int index, T entity);
	
	/**
	 * Updates the entity in the extent without flushing changes.
	 * 
	 * @param index Position of the extent to be updated.
	 * @param entity Entity to replace the old one.
	 * @return Entity previously at the specified position.
	 */
	protected abstract T doSet(int index, T entity);
	
	/**
	 * Removes the entity at the specified position from the extent without flushing changes.
	 * 
	 * @param index Position of the extent to be removed.
	 * @return Entity previously at the specified position.
	 */
	protected abstract T doRemove(int index);
	
	/**
	 * Removes the entity specified entity from the extent without flushing changes.
	 * 
	 * @param entity Entity to remove.
	 * @return True if removed.
	 */
	protected abstract boolean doRemove(T entity);
	
	/**
	 * Retrieves the entity at the specified position in the extent.
	 * 
	 * @param index Position of the entity to be retrieved.
	 * @return Entity retrieved.
	 */
	protected abstract T doGet(int index);
	
	/**
	 * Indicates if the specified entity exists in the extent.
	 * 
	 * @param entity Entity to verify.
	 * @return True if the extent contains the entity.
	 */
	protected abstract boolean doContains(T entity);
	
	/**
	 * @return The number of entities in the extent with changes applied but not flushed.
	 */
	protected abstract int doSize();
	
	/**
	 * Invoked after {@link #flush()} to provide callback functionality to subclasses.
	 */
	protected abstract void flushed();
	
	// Getters/Setters

	/**
	 * {@inheritDoc}
	 */
	public Class<T> getEntityType() {
		return entityType;
	}

	/**
	 * @return Helper to execute JPA operations.
	 */
	public JpaOperations getJpaOperations() {
		return jpaOperations;
	}

}
